﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections.ObjectModel;

namespace Luminal
{
    public sealed class Context : LuminalObject
    {
        #region Interop
        [DllImport("OpenCL.dll")]
        static extern IntPtr clCreateContext(IntPtr[] properties,
            uint num_devices,
            IntPtr[] devices,
            IntPtr pfnNotify,
            IntPtr user_data,
            out ErrorCode errcode_ret);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clReleaseContext(IntPtr context);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetContextInfo(IntPtr context,
            cl_context_info param_name,
            SizeT param_value_size,
            IntPtr param_value,
            out SizeT param_value_size_ret);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetSupportedImageFormats(IntPtr context,
            cl_mem_flags flags,
            MemoryObjectType image_type,
            uint num_entries,
            IntPtr image_formats,
            out uint num_image_formats);
        #endregion

        #region Properties
        public PlatformIdentifier Platform { get; private set; }
        public ReadOnlyCollection<DeviceIdentifier> Devices { get; private set; }

        private T[] GetInfo<T>(cl_context_info info) where T : struct
        {
            SizeT infoSize = 0;
            Util.ThrowError("Could not get device property.", clGetContextInfo(InternalPointer, info, 0, IntPtr.Zero, out infoSize));


            T[] output = new T[infoSize / Marshal.SizeOf(typeof(T))];
            Util.Pin(output, ptr =>
            {
                Util.ThrowError("Could not get device property.", clGetContextInfo(InternalPointer, info, infoSize, ptr, out infoSize));
            });

            return output;
        }
        #endregion

        #region Constructor
        public Context(PlatformIdentifier platform, DeviceType deviceType)
            : this(platform, platform.GetDevices(deviceType))
        {
            // Nothing here - this is easier than wrapping clCreateContextFromType
        }

        public Context(PlatformIdentifier platform, DeviceIdentifier[] devices)
        {
            if (platform == null)
                throw new ArgumentNullException("platform");
            if (platform.Disposed)
                throw new ObjectDisposedException("platform");
            if (devices == null)
                throw new ArgumentNullException("devices");
            if (devices.Any(n => n.Disposed))
                throw new ObjectDisposedException("devices", "One or more of the specified devices is disposed.");
            if (devices.Any(n => n == null))
                throw new ArgumentNullException("devices", "One or more of the specified devices is null.");
            if (devices.Length == 0)
                throw new ArgumentException("Parameter must have at least one element.", "devices");
            
            IntPtr[] properties = new IntPtr[]
            {
                new IntPtr(0x1084), // CL_CONTEXT_PLATFORM
                platform.InternalPointer,
                IntPtr.Zero,
            };

            IntPtr[] devicePtrs = Util.ExtractPointers<DeviceIdentifier>(devices);

            ErrorCode errorCode;
            Initialize(clCreateContext(properties, (uint)devices.Length, devicePtrs, IntPtr.Zero, IntPtr.Zero, out errorCode));

            try
            {
                Util.ThrowError(
                    "Could not create context.", 
                    errorCode,
                    new ErrorCase(ErrorCode.CL_DEVICE_NOT_AVAILABLE, new DeviceNotAvailableException("One or more of the specified devices is not currently available.")));
            }
            catch (Exception ex)
            {
                Dispose();
                throw ex;
            }

            Platform = platform;
            Devices = new ReadOnlyCollection<DeviceIdentifier>(devices);
        }

        private Context(IntPtr ptr)
        {
            Initialize(ptr);

            try
            {
                IntPtr[] properties = GetInfo<IntPtr>(cl_context_info.CL_CONTEXT_PROPERTIES);
                Platform = PlatformIdentifier.FromPointer(properties[1]);

                IntPtr[] devices = GetInfo<IntPtr>(cl_context_info.CL_CONTEXT_DEVICES);
                Devices = devices.Select(n => DeviceIdentifier.FromPointer(n)).ToList().AsReadOnly();
            }
            catch (Exception ex)
            {
                Dispose();
                throw ex;
            }
        }
        #endregion

        #region Methods
        public ImageFormat[] GetSupportedImageFormats(MemoryObjectType objectType, MemoryAccessFlags accessFlags)
        {
            if (Disposed)
                throw new ObjectDisposedException(GetType().FullName);

            if (objectType == MemoryObjectType.Buffer)
                throw new ArgumentException("The specified argument type cannot be MemoryObjectType.Buffer.", "objectType");

            uint outputCount;
            Util.ThrowError(
                "Could not get image formats.",
                clGetSupportedImageFormats(InternalPointer, (cl_mem_flags)accessFlags | cl_mem_flags.CL_MEM_ALLOC_HOST_PTR, objectType, 0, IntPtr.Zero, out outputCount));

            ImageFormat[] output = new ImageFormat[outputCount];
            Util.Pin(output, ptr =>
                {
                    Util.ThrowError(
                        "Could not get image formats.",
                        clGetSupportedImageFormats(InternalPointer, (cl_mem_flags)accessFlags | cl_mem_flags.CL_MEM_ALLOC_HOST_PTR, objectType, outputCount, ptr, out outputCount));
                });
            return output;
        }

        public bool CheckImageFormatSupport(ImageFormat format, MemoryObjectType objectType, MemoryAccessFlags accessFlags)
        {
            if (Disposed)
                throw new ObjectDisposedException(GetType().FullName);

            if (objectType == MemoryObjectType.Buffer)
                throw new ArgumentException("The specified argument type cannot be MemoryObjectType.Buffer.", "objectType");

            return GetSupportedImageFormats(objectType, accessFlags).Contains(format);
        }
        #endregion

        protected override void Dispose(bool disposing)
        {
            if (disposing)
                Util.ThrowError("Could not release context.", clReleaseContext(InternalPointer));

            base.Dispose(disposing);
        }

        public static Context FromPointer(IntPtr ptr)
        {
            return ObjectTracker.ContainsObject(ptr) ? ObjectTracker.GetObject<Context>(ptr) : new Context(ptr);
        }
    }
}
